/*******************************************************************************
 * Copyright (c) 2013-2017 Contributors to the Eclipse Foundation
 * 
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License,
 * Version 2.0 which accompanies this distribution and is available at
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 ******************************************************************************/
package mil.nga.giat.geowave.core.cli.prefix;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameterized;

import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.bytecode.AccessFlag;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.BooleanMemberValue;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.StringMemberValue;

/**
 * The translation map allows us to easily copy values from the facade objects
 * back to the original objects.
 */
public class JCommanderTranslationMap
{

	private static Logger LOGGER = LoggerFactory.getLogger(JCommanderTranslationMap.class);

	// This package is where classes generated by this translator live in the
	// classpath.
	public static final String NAMES_MEMBER = "names";
	public static final String REQUIRED_MEMBER = "required";
	// HP Fortify "Hardcoded Password" false positive
	// This is a password label, not a password
	public static final String PASSWORD_MEMBER = "password";
	public static final String PREFIX_SEPARATOR = ".";

	// Tells us how to translate a field (indexed by facade field id) to
	// the original objects and back.
	private final Map<String, TranslationEntry> translations = new LinkedHashMap<String, TranslationEntry>();

	// These are the objects generated by createFacadeObjects()
	private List<Object> translatedObjects = null;

	public JCommanderTranslationMap() {

	}

	/**
	 * Objects are the facades.
	 * 
	 * @return
	 */
	public Collection<Object> getObjects() {
		return Collections.unmodifiableCollection(translatedObjects);
	}

	/**
	 * Return all the translations. They are indexed by 'field name', where
	 * field name is the field in the facade object. Allow the user to modify
	 * them up until they create the facade objects
	 * 
	 * @return
	 */
	public Map<String, TranslationEntry> getEntries() {
		if (translatedObjects != null) {
			return Collections.unmodifiableMap(translations);
		}
		return translations;
	}

	/**
	 * Transfer the values from the facade objects to the original objects using
	 * the translation map.
	 */
	public void transformToOriginal() {
		for (Object obj : translatedObjects) {
			for (Field field : obj.getClass().getDeclaredFields()) {
				TranslationEntry tEntry = translations.get(field.getName());
				try {
					tEntry.getParam().set(
							tEntry.getObject(),
							field.get(obj));
				}
				catch (IllegalArgumentException | IllegalAccessException e) {
					// Allow these, since they really shouldn't ever happen.
					LOGGER.warn(
							"Unable to return field object",
							e);
				}
			}
		}
	}

	/**
	 * Transfer the values from the original objects to the facade objects using
	 * the translation map.
	 */
	public void transformToFacade() {
		for (Object obj : translatedObjects) {
			for (Field field : obj.getClass().getDeclaredFields()) {
				TranslationEntry tEntry = translations.get(field.getName());
				try {
					field.set(
							obj,
							tEntry.getParam().get(
									tEntry.getObject()));
				}
				catch (IllegalArgumentException | IllegalAccessException e) {
					// Ignore, no getter (if it's a method) or there was
					// a security violation.
					LOGGER.warn(
							"Unable to set field",
							e);
				}
			}
		}
	}

	/**
	 * This is a mapping between the created facade's field (e.g., field_0) and
	 * the JCommander parameter (param) which lives in the object it was parsed
	 * from, 'item'.
	 * 
	 * @param newFieldName
	 * @param item
	 * @param param
	 * @param names
	 *            - the arguments values for this item.
	 */
	protected void addEntry(
			String newFieldName,
			Object item,
			Parameterized param,
			String prefix,
			AnnotatedElement member ) {

		translations.put(
				newFieldName,
				new TranslationEntry(
						param,
						item,
						prefix,
						member));
	}

	/**
	 * This will create the facade objects needed in order to parse the fields
	 * represented in the translation map.
	 * 
	 * @param map
	 * @return
	 */
	public void createFacadeObjects() {
		if (translatedObjects != null) {
			throw new RuntimeException(
					"Cannot use the same translation " + "map twice");
		}

		// Clear old objects.
		translatedObjects = new ArrayList<>();

		// So we don't re-create classes we already created.
		Map<Class<?>, CtClass> createdClasses = new HashMap<Class<?>, CtClass>();

		try {

			// This class pool will be used to find existing classes and create
			// new
			// classes.
			ClassPool classPool = ClassPool.getDefault();
			ClassClassPath path = new ClassClassPath(
					JCommanderPrefixTranslator.class);
			classPool.insertClassPath(path);

			// Iterate the final translations and create the classes.
			for (Map.Entry<String, TranslationEntry> mapEntry : translations.entrySet()) {

				// Cache for later.
				String newFieldName = mapEntry.getKey();
				TranslationEntry entry = mapEntry.getValue();

				// This is the class we're making a facade of.
				Class<?> objectClass = entry.getObject().getClass();

				// Get a CtClass reference to the item's class
				CtClass oldClass = classPool.get(objectClass.getName());

				// Retrieve previously created class to add new field
				CtClass newClass = createdClasses.get(objectClass);

				// Create the class if we haven't yet.
				if (newClass == null) {

					// Create the class, so we can start adding the new facade
					// fields to it.
					newClass = JavassistUtils.generateEmptyClass();

					// Copy over the @Parameters annotation, if it is set.
					JavassistUtils.copyClassAnnotations(
							oldClass,
							newClass);

					// Store for later.
					createdClasses.put(
							objectClass,
							newClass);
				}

				// This is a field or method, which means we should add it to
				// our current
				// object.
				CtField newField = null;
				if (!entry.isMethod()) {
					// This is a field. This is easy! Just clone the field. It
					// will
					// copy over the annotations as well.
					newField = new CtField(
							oldClass.getField(entry.getParam().getName()),
							newClass);
				}
				else {
					// This is a method. This is hard. We can create a field
					// with the same name, but we gotta copy over the
					// annotations manually.
					// We also don't want to copy annotations that specifically
					// target
					// METHOD, so we'll only clone annotations that can target
					// FIELD.
					CtClass fieldType = classPool.get(entry.getParam().getType().getName());
					newField = new CtField(
							fieldType,
							entry.getParam().getName(),
							newClass);

					// We need to find the existing method CtMethod reference,
					// so we can clone
					// annotations. This method is ugly. Do not look at it.
					CtMethod method = JavassistUtils.findMethod(
							oldClass,
							(Method) entry.getMember());

					// Copy the annotations!
					JavassistUtils.copyMethodAnnotationsToField(
							method,
							newField);
				}

				// This is where the meat of the prefix algorithm is. If we have
				// a prefix
				// for this class(in ParseContext), then we apply it to the
				// attributes by
				// iterating over the annotations, looking for a 'names' member
				// variable, and
				// overriding the values one by one.
				if (entry.getPrefix().length() > 0) {
					overrideParameterPrefixes(
							newField,
							entry.getPrefixedNames());
				}

				// This is a fix for #95 (
				// https://github.com/cbeust/jcommander/issues/95 ).
				// I need this for cpstore, cpindex, etc, but it's only been
				// implemented as of 1.55,
				// an unreleased version.
				if (entry.isRequired() && entry.hasValue()) {
					disableBooleanMember(
							REQUIRED_MEMBER,
							newField);
				}

				if (entry.isPassword() && entry.hasValue()) {
					disableBooleanMember(
							PASSWORD_MEMBER,
							newField);
				}

				// Rename the field so there are no conflicts. Name really
				// doesn't matter,
				// but it's used for translation in transMap.
				newField.setName(newFieldName);
				newField.getFieldInfo().setAccessFlags(
						AccessFlag.PUBLIC);

				// Add the field to the class
				newClass.addField(newField);

			} // Iterate TranslationEntry

			// Convert the translated CtClass to an actual class.
			for (CtClass clz : createdClasses.values()) {
				Class<?> toClass = clz.toClass();
				Object instance = toClass.newInstance();
				translatedObjects.add(instance);
			}
		}
		catch (InstantiationException | IllegalAccessException | NotFoundException | IllegalStateException
				| NullPointerException | CannotCompileException e) {
			LOGGER.error(
					"Unable to create classes",
					e);
			throw new RuntimeException();
		}
		/*
		 * catch (Exception e) { // This should never happen, but if it does,
		 * then it's a programmer // error. throw new RuntimeException( e); }
		 */
	}

	/**
	 * Iterate the annotations, look for a 'names' parameter, and override it to
	 * prepend the given prefix.
	 * 
	 * @param field
	 * @param prefix
	 */
	private void overrideParameterPrefixes(
			CtField field,
			String[] names ) {

		// This is the JCommander package name
		String packageName = JCommander.class.getPackage().getName();

		AnnotationsAttribute fieldAttributes = (AnnotationsAttribute) field.getFieldInfo().getAttribute(
				AnnotationsAttribute.visibleTag);

		// Look for annotations that have a 'names' attribute, and whose package
		// starts with the expected JCommander package.
		for (Annotation annotation : fieldAttributes.getAnnotations()) {
			if (annotation.getTypeName().startsWith(
					packageName)) {
				// See if it has a 'names' member variable.
				MemberValue namesMember = annotation.getMemberValue(NAMES_MEMBER);

				// We have a names member!!!
				if (namesMember != null) {
					ArrayMemberValue arrayNamesMember = (ArrayMemberValue) namesMember;

					// Iterate and transform each item in 'names()' list and
					// transform it.
					MemberValue[] newMemberValues = new MemberValue[names.length];
					for (int i = 0; i < names.length; i++) {
						newMemberValues[i] = new StringMemberValue(
								names[i],
								field.getFieldInfo2().getConstPool());
					}

					// Override the member values in nameMember with the new
					// one's we've generated
					arrayNamesMember.setValue(newMemberValues);

					// This is KEY! For some reason, the existing annotation
					// will not be modified unless
					// you call 'setAnnotation' here. I'm guessing
					// 'getAnnotation()' creates a copy.
					fieldAttributes.setAnnotation(annotation);

					// Finished processing names.
					break;
				}
			}
		}
	}

	/**
	 * Iterate the annotations, look for a 'required' parameter, and set it to
	 * false.
	 * 
	 * @param field
	 * @param prefix
	 */
	private void disableBooleanMember(
			String booleanMemberName,
			CtField field ) {

		// This is the JCommander package name
		String packageName = JCommander.class.getPackage().getName();

		AnnotationsAttribute fieldAttributes = (AnnotationsAttribute) field.getFieldInfo().getAttribute(
				AnnotationsAttribute.visibleTag);

		// Look for annotations that have a 'names' attribute, and whose package
		// starts with the expected JCommander package.
		for (Annotation annotation : fieldAttributes.getAnnotations()) {
			if (annotation.getTypeName().startsWith(
					packageName)) {
				// See if it has a 'names' member variable.
				MemberValue requiredMember = annotation.getMemberValue(booleanMemberName);

				// We have a names member!!!
				if (requiredMember != null) {
					BooleanMemberValue booleanRequiredMember = (BooleanMemberValue) requiredMember;

					// Set it to not required.
					booleanRequiredMember.setValue(false);

					// This is KEY! For some reason, the existing annotation
					// will not be modified unless
					// you call 'setAnnotation' here. I'm guessing
					// 'getAnnotation()' creates a copy.
					fieldAttributes.setAnnotation(annotation);

					// Finished processing names.
					break;
				}
			}
		}
	}

}
